package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.dto.ScrollResult;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.Blog;
import com.hmdp.entity.Follow;
import com.hmdp.entity.User;
import com.hmdp.mapper.BlogMapper;
import com.hmdp.service.IBlogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IFollowService;
import com.hmdp.service.IUserService;
import com.hmdp.utils.SystemConstants;
import com.hmdp.utils.UserHolder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import static com.hmdp.utils.RedisConstants.BLOG_LIKED_KEY;
import static com.hmdp.utils.RedisConstants.FEED_KEY;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {

    @Resource
    private IUserService userService; // 注入用户服务接口
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private IFollowService followService;

    /**
     * 查询热门博客
     *
     * @param current 当前页码
     * @return 返回封装了博客列表的结果对象
     */
    @Override
    public Result queryHotBlog(Integer current) {
        // 分页查询博客，按点赞数降序排列
        Page<Blog> page = query()
                .orderByDesc("liked")
                .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
        // 获取当前页的博客数据
        List<Blog> records = page.getRecords();
        // 遍历博客列表，为每篇博客查询并设置相关用户信息
        records.forEach(blog -> {
            queryBlogUser(blog);
            // 查询Blog是否被点赞
            isBlogLike(blog);
        });
        return Result.ok(records); // 返回成功结果和博客列表
    }

    /**
     * 根据博客ID查询博客详情
     *
     * @param id 博客ID
     * @return 返回封装了博客详情的结果对象
     */
    @Override
    public Result QueryBlogById(Long id) {
        // 根据ID查询博客
        Blog blog = getById(id);
        if (blog == null) {
            return Result.fail("笔记不存在"); // 如果博客不存在，返回失败结果
        }
        // 查询并设置博客相关用户信息
        queryBlogUser(blog);
        // 查询Blog是否被点赞
        isBlogLike(blog);
        return Result.ok(blog); // 返回成功结果和博客详情
    }

    /**
     * 判断博客是否被点赞
     * @param blog 1
     */
    private void isBlogLike(Blog blog) {
        // 获取当前登录用户的ID
        UserDTO user = UserHolder.getUser();
        if(user == null){
            //用户未登录，无需查询是否点赞
            return ;
        }
        Long userId = user.getId();
        // 构建Redis中的key，用于存储该博客的点赞用户集合
        String key = BLOG_LIKED_KEY + blog.getId() ;

        // 使用Redis的Set数据结构查询当前用户是否已经点赞该博客
        // 对应的Redis命令：SISMEMBER key userId
        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());

        // 设置博客的isLike属性，根据查询结果判断用户是否已点赞
        // 如果isMember为true，则表示用户已点赞，设置isLike为true；否则为false
        blog.setIsLike(score!= null);
    }


    /**
     * 判断当前用户是否已经点赞
     *
     * @param id 1
     * @return true or false
     */
    @Override
    public Result likeBlog(Long id) {
        // 获取登录用户
        Long userId = UserHolder.getUser().getId();
        // 构建Redis中的key，用于存储该id的笔记的点赞状态
        String key = BLOG_LIKED_KEY + id ;
        // 判断当前用户是否已点赞
        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
        if (score == null)  {
            // 用户未点赞 允许点赞 数据库点赞数＋1
            boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
            // 保存用户至Redis的set集合中
            if (isSuccess) {// 执行Redis命令：ZADD key userId SCORE
                stringRedisTemplate.opsForZSet().add(key, userId.toString(),System.currentTimeMillis());
            }
        } else {
            // 用户已点赞 取消点赞 数据库点赞数-1
            boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
            // 删除Redis的set集合中的用户
            if (isSuccess) {// 执行Redis命令：SREM key userId
                stringRedisTemplate.opsForZSet().remove(key, userId.toString());
            }
        }
        return Result.ok();
    }

    /**
     * 根据笔记id查询点赞top的用户
     * @param id 1
     * @return 1
     */
    @Override
    public Result queryBlogLikes(Long id) {
        // 构建Redis中存储博客点赞信息的键，其中BLOG_LIKED_KEY是常量，表示点赞信息的前缀
        String key = BLOG_LIKED_KEY + id;

        // 使用Redis的ZSet数据结构查询点赞数排名前5的用户ID
        // 对应的Redis命令为：ZRANGE key 0 4，表示获取键为key的ZSet中排名0到4的成员
        Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);

        // 如果没有点赞用户，直接返回空列表
        if (top5 == null || top5.isEmpty()) {
            return Result.ok(Collections.emptyList());
        }

        // 将Set中的用户ID字符串转换为Long类型列表
        List<Long> ids = top5.stream() // 将Set转换为Stream流
                .map(Long::valueOf) // 将每个字符串ID转换为Long类型
                .collect(Collectors.toList()); // 收集转换后的Long类型ID到列表中

        // 将用户ID列表转换为以逗号分隔的字符串，用于SQL语句中的FIELD函数
        String idStr = StrUtil.join(",", ids);

        // 根据用户ID列表查询用户详细信息
        // 使用MyBatis-Plus的查询构造器，in("id", ids)表示ID在列表ids中
        // last("ORDER BY FIELD(id,"+idStr+")")用于指定排序，按照FIELD函数的返回值排序，保持与Redis中相同的顺序
        List<UserDTO> userDTOS = userService.query()
                .in("id", ids).last("ORDER BY FIELD(id," + idStr + ")")
                .list() // 执行查询并获取结果列表
                .stream() // 将结果列表转换为Stream流
                // 将User实体类转换为UserDTO数据传输对象
                .map(user -> BeanUtil.copyProperties(user, UserDTO.class)) // 复制属性到UserDTO对象
                .collect(Collectors.toList()); // 收集转换后的UserDTO对象到列表中

        // 返回查询到的用户DTO列表
        return Result.ok(userDTOS);
    }

    @Override
    public Result saveBlog(Blog blog) {
        // 获取当前登录用户的信息
        UserDTO user = UserHolder.getUser();
        // 设置博文的用户ID为当前登录用户的ID
        blog.setUserId(user.getId());
        if (blog.getShopId()== null) {
            return Result.fail("关联店铺不能为空");
        } else if (blog.getTitle() == null) {
            return Result.fail("标题不能为空");
        }else if (blog.getContent() == null) {
            return Result.fail("内容不能为空");
        }
        // 保存探店博文到数据库
        boolean isSuccess = save(blog);
        // 检查博文是否保存成功
        if (!isSuccess) {
            // 如果保存失败，返回失败结果
            return Result.fail("新增笔记失败!");
        }
        // 查询当前笔记作者的所有粉丝
        List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
        // 遍历所有粉丝
        for (Follow follow : follows) {
            // 获取每个粉丝的用户ID
            Long userId = follow.getUserId();
            // 构造用于存储推送博文的Redis键
            String key = FEED_KEY + userId;
            // 将博文ID和当前时间戳添加到对应粉丝的Feed流中
            stringRedisTemplate.opsForZSet().add(key, blog.getId().toString(), System.currentTimeMillis());
        }
        // 博文保存成功，返回包含博文ID的成功结果
        return Result.ok(blog.getId());
    }

    @Override
    public Result queryBlogOfFollow(Long max, Integer offset) {
        // 获取当前用户的ID
        Long userId = UserHolder.getUser().getId();

        // 构造用于查询的Redis键
        String key = FEED_KEY + userId;

        // 从Redis中查询当前用户收件箱的数据，使用ZREVRANGEBYSCORE命令逆序获取分数在0到max之间的元素，限制返回的数量为3，并跳过offset个元素
        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet()
                .reverseRangeByScoreWithScores(key, 0, max, offset, 3);

        // 检查查询结果是否为空
        if (typedTuples == null || typedTuples.isEmpty()) {
            // 如果为空，直接返回
            return Result.ok();
        }

        // 初始化用于存储博客ID的列表，以及用于记录最小时间戳和相同时间戳数量的变量
        List<Object> ids = new ArrayList<>(typedTuples.size());
        long minTime = max; // 初始最小时间戳设为最大时间戳
        int sameNum = 1; // 初始相同时间戳数量为1

        // 遍历查询结果，解析数据
        for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
            // 将博客ID添加到列表中
            ids.add(Long.valueOf(typedTuple.getValue()));

            // 获取当前元素的时间戳（分数）
            long time = typedTuple.getScore().longValue();

            // 如果当前时间戳与最小时间戳相同，增加相同时间戳数量
            if (time == minTime) {
                sameNum++;
            } else {
                // 否则，更新最小时间戳并重置相同时间戳数量
                minTime = time;
                sameNum = 1;
            }
        }

        // 根据解析出的博客ID查询博客详情，并按照ID的顺序排序
        String idStr = StrUtil.join(",", ids); // 将ID列表转换为逗号分隔的字符串
        List<Blog> blogs = query()
                .in("id", ids).last("ORDER BY FIELD(id," + idStr + ")") // 使用FIELD函数按照指定顺序排序
                .list(); // 执行查询并获取结果列表

        // 遍历博客列表，填充每篇博客的详细信息
        for (Blog blog : blogs) {
            // 查询并设置与博客相关的用户信息
            queryBlogUser(blog);

            // 查询并设置博客是否被当前用户点赞
            isBlogLike(blog);
        }

        // 创建滚动结果对象，设置博客列表、偏移量和最小时间戳
        ScrollResult scrollResult = new ScrollResult();
        scrollResult.setList(blogs);
        scrollResult.setOffset(sameNum); // 更新偏移量
        scrollResult.setMinTime(minTime); // 设置最小时间戳

        // 封装滚动结果并返回成功的结果
        return Result.ok(scrollResult);
    }



    /**
     * 为博客查询并设置相关用户信息
     * @param blog 博客对象
     */
    private void queryBlogUser(Blog blog) {
        Long userId = blog.getUserId(); // 获取博客的用户ID
        User user = userService.getById(userId); // 根据用户ID查询用户信息
        blog.setName(user.getNickName()); // 设置博客的用户昵称
        blog.setIcon(user.getIcon()); // 设置博客的用户头像
    }
}
